
class Counter{
    public int count = 0;

    // 没有调成安全前的自增
    /*public void add(){
        count++;
    }*/

    // 调整线程安全后的自增

    // 方法一 加锁 让 synchronized 修饰方法
   /* synchronized public void add(){  // synchronized 这个关键字 在java中就是表示 加锁 的意思 （要会读会写）
        count++;                   // synchronized修饰方法
    }*/

    //synchronized 加了这个关键字之后 进入方法就会加锁，出了方法就会解锁
    // 如果两个线程同时尝试加锁，此时一个能获取锁成功，另一个只能阻塞等待（BLOCKED） 一直阻塞到刚才的线程释放锁（解锁），当前的线程才能加锁成功
    // (就像两个人着急上厕所)

    // 方法二 加锁 让 synchronized 修饰代码块
    public void add(){
        synchronized (this){
            count++;  // 表示进入代码块就加锁，出了代码块就解锁，this就是我们加锁的对象，当然可以指定任意我们想指定的对象
            // 针对代码块加锁就是指我们想对我们想要的对象进行加锁

            // 代码块就是大括号 {}
            // 代码块也可以有快捷键 ctrl alt + t 键生成
            // 具体使用是，选中代码，然后用快捷键，就可以选择一个代码块来包裹选中的代码
        }

        // 是否可重入呢 ？
         // 如果代码这样写会怎么样？
        // synchronized public void add(){
        //        synchronized (this){
        //            count++;
        //        }
        // 此处是特殊情况，第二个线程和第一个线程其实是同一个线程。这里考虑一下是否要特殊开个绿灯呢? ()
        // 如果允许上述操作，这个锁就是可重入的
        // 如果不允许上述操作（第二次加锁会阻塞等待），就是不可重入的，那这种情况就会导致线程陷入了“死锁”状态了
    }

    // 接下来写一个 一个线程加锁一个线程不加锁的情况
    public void add2(){
        count++;
    }

    // （加锁操作的执行代码速度一定是大大折扣的）

}
public class ThreadDemo5 {
    public static void main(String[] args) {
        // 多线程安全问题
        Counter counter = new Counter();
        // 搞两个线程 ，两个线程分别针对 counter 来调用 5w 次的 add 方法

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {  // i < 50000 也是一个操作系统指令 i++又是一个单独的操作系统指令
                counter.add();  // 加了 synchronized 之后 执行add加上了锁。针对counter这个对象加上了锁。
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();  // 这时候 thread2 在执行add 的时候，也尝试对 counter 加锁。但是由于 counter 已经被thread1占用了
                // 因此这里的加锁操作就会被阻塞

                // 这个时候如果线程thread2调用的是add2的方法就表示一个线程加锁一个线程不加锁的情况了
                //counter.add2();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程的结束
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 打印最终的 count 的值
        System.out.println("count = "+counter.count);

        // 我们期望看到的值是 count = 100000 （10万次）
        // 而最后结果却是 53053 次 （而且还是随机的）

        // 所以这里面执行的结果就和预期的结果不一样了，所以这里就是一个线程安全问题
        // 这个我们也就称之为bug

        // 但是为什么程序就出现这个问题了呢？
        // 要先看 count++; 的操作
        // 实际上的++操作要分成三步：
        // 1.先把内存中的值，读取到CPU的寄存器当中  （load操作）
        // 2.把CPU寄存器里的数值进行+1运算    （add操作）           --—--> 这三个操作，就是CPU上执行的三个指令
        // 3.把得到的结果写回到内存当中       （save操作）

        // 但是 如果我们是 两个线程并发的执行count++操作 就相当于两组 load add save 的操作进行执行 （穿插执行）（结果不断被覆盖叠加）
        // 此时不同的线程调度顺序就可能会产生一些结果上的差异

        // 那么当前的代码是否可能正好就是十万呢？ 也是有可能的，但是概率非常小
        // 那么当前这个结果一定会大于5w么？实际运行之后发现基本上都是大于5w的，但是确实也不一定

        // 那么到底是什么样的情况会出现线程安全问题？是不是所有的多线程都会涉及到线程安全问题？
        // 1【根本原因】抢占式执行，随机调度.这个原因我们无能为力
        // 2 代码结构：多个线程会同时修改同一个变量（所以一个线程修改一个变量，多个线程读取同一个变量，多个线程修改多个变量 等等都没事）
        //  (因此可以通过调整代码结构来规避这个问题，这种调整，不一定都能使用的，代码结构也是来源于 需求 的)
        // 3 原子性：如果修改的操作是原子的，那还罢了，但如果是非原子的，出现问题的概率就非常高了！（原子：不可拆分的基本单位） (count++就是非原子的)
        // （针对线程安全问题，如何解决？最主要的手段就是从这个原子性入手，把这个非原子的操作变成原子的~ 加锁（跟前面的数据库一样）。）
        // 4 内存可见性问题
        // （如果是一个线程读，一个线程改呢？也会出现问题，可能出现此处读的结果不太符合预期）
        // 5. 指令重排序（本质上是编译器优化出bug了 （编译器觉得你自己写的代码太垃圾了，就把你的代码自作主张的调整了，保持逻辑不变的情况下，进行调整，从而加快程序的执行效率））
        // （经常会调整代码的调整顺序） (重排序指的是单个线程里顺序发生调整)

        // 上述分析的是5个典型例子的原因。不是全部，到底线程安全不安全，只能是具体代码具体分析
        // （这里需要在脑子里分析出可能执行的情况，并判定里面是否有问题）


        // 那么如何从原子性入手来解决线程操作？
        // 加锁！！！ ，通过加锁把不是原子的转换成“原子”的

        // 加锁的地方就是 在 load 之前先加锁 （lock操作）
        // 解锁就是在 save 之后解锁（unlock）
        // 第一个线程正在lock的时候第二个线程的加锁操作就不会加成 ， 会一直阻塞等待，一直等到第一个线程unlock之后，这里的lock才会继续执行
        // （就是只有一个线程提交数据之后并且释放锁之后才能让另一个线程来读）

        // 加锁 说是保持原子性，其实不是说让这里的三个操作一个完成，也不是这三步操作过程部件进行调度，而是让其他也想操作的线程阻塞等待了...
        // 加锁的本质是把并行变成了串行

        // （synchronized 的行为就是阻塞等待，一直等待下去，java还有一种锁就是ReentrantLock 这个锁是可以获取不到解锁就放弃的操作）

        // 然而加锁之后呢： 我们只是给add 加锁了，其余的 for 循环和比较和自增操作都是不必加锁的，是可以并发执行的 ，因为都是每个线程的局部变量

    }

}
